%%% Copyright 2010 Google Inc.
%%%
%%% Licensed under the Apache License, Version 2.0 (the "License");
%%% you may not use this file except in compliance with the License.
%%% You may obtain a copy of the License at
%%%
%%%     http://www.apache.org/licenses/LICENSE-2.0
%%%
%%% Unless required by applicable law or agreed to in writing, software
%%% distributed under the License is distributed on an "AS IS" BASIS,
%%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
%%% See the License for the specific language governing permissions and
%%% limitations under the License.

%%% @copyright 2010 Google Inc.
%%% @author <jacoby@google.com> Jacoby Thwaites [http://sites/madcloud]
%%% @version {@version}
%%% @doc
%%% ETS-based cache server for in-process reads and serialized writes
%%% of data accessed in-process when in-cache.
%%%
%%% This is used to provide the process cache and view cache, for example.
%%%
%%% Implementation modules, as specified in start_link/1, shd provide:
%%% Mod:keypos() - returns integer key position for record lookup.
%%% Mod:generate(Key) - generates and returns a new term to be cached
%%% against key.
%%%
%%% @headerfile "../include/txm.hrl"
%%% @headerfile "../include/cache_srv.hrl"

-module(cache_srv).

-behaviour(gen_server).

-include("txm.hrl").
-include("cache_srv.hrl").

%% Start, stop and client functions.
-export([start_link/1, stop/1]).
-export([get_cache_record/2, get_cache_state/1]).
-export([generate/2]).
-export([poke_cache_record/2, wipe_cache_record/2]).

%% Callback functions.
-export([init/1]).
-export([handle_call/3, terminate/2]).
-export([handle_cast/2, handle_info/2, code_change/3]).

%%% ---------------------------------------------------------------------------
%%% Start and stop functions
%%% ---------------------------------------------------------------------------

%% @doc Starts the cache server linked to the calling process.
%% @spec start_link(atom()) -> {ok, pid()}
start_link(CacheName) ->
  gen_server:start_link({local, CacheName}, ?MODULE, CacheName, []).

%% @doc Stops the cache server.
%% @spec stop(atom()) -> ok
stop(CacheName) ->
  gen_server:call(CacheName, ?CACHE_STOP).

%%% ---------------------------------------------------------------------------
%%% Client functions
%%% ---------------------------------------------------------------------------

%% @doc Get the cache record matching `Key' from cache in-process.
%% Otheriwse generate in the cache_srv process and write to cache.
%% Updates cache hits and misses as appropriate.
%% @spec get_cache_record(atom(), ref()) -> #cache_record{}
get_cache_record(CacheName, Key) ->
  case ets:lookup(CacheName, Key) of
    [CacheRecord] ->
      gen_server:cast(CacheName, ?CACHE_HIT),
      CacheRecord;
    [] ->
      generate(CacheName, Key)
  end.

%% @doc Generates a cache entry. This is done in the cache_srv process
%% so that resulting ETS tables eg in digraphs are owned by this persistent
%% process.
generate(CacheName, Key) ->
  gen_server:call(CacheName, {?CACHE_GENERATE, Key}, infinity).

%% @doc Poke the supplied record directly into the cache, replacing
%% any existing record with the same key if present.
poke_cache_record(CacheName, Record) ->
  gen_server:call(CacheName, {?CACHE_POKE, Record}).

%% @doc Wipes the matching record (not specified just by key) directly
%% from the cache.
wipe_cache_record(CacheName, Record) ->
  gen_server:call(CacheName, {?CACHE_WIPE, Record}).

%% @doc Returns the cache performance stats.
%% @spec get_cache_state(atom()) -> #cache_state{}
get_cache_state(CacheName) ->
  gen_server:call(CacheName, ?CACHE_STATE).

%%% ---------------------------------------------------------------------------
%%% Callback gen_server functions
%%% ---------------------------------------------------------------------------

%% @doc The process server is initialized against the composition of the
%% supplied folder pathname or reference.
%% @spec init(term()) -> {ok, #cache_state{}}
init(CacheName) ->
  ?INFO([init, {cache, CacheName}]),

  % Ensure that stop() causes terminate() callback from supervisor.
  process_flag(trap_exit, true),

  % Create the protected ETS table (all processes read, only this process
  % writes).
  ?INFO([create_table, {ets, CacheName}]),
  ets:new(CacheName,
          [set,
           protected,
           named_table,
           {keypos, CacheName:keypos()}]),
  ?OK_RETURN(#?CACHE_STATE{name = CacheName}).

%% On terminate we delete the cache table.
terminate(_Reason, #?CACHE_STATE{name = CacheName}) ->
  ?INFO([{terminate, CacheName}]),
  ets:delete(CacheName),
  ?OK_RETURN.

%%% ---------------------------------------------------------------------------
%%% Asynchronous cache handlers.
%%% ---------------------------------------------------------------------------

%% @doc Increment the cache hit count.
handle_cast(?CACHE_HIT,
            #?CACHE_STATE{hits = Hits} = State) ->
  ?DEBUG([{?CACHE_HIT, State}]),
  {noreply, State#?CACHE_STATE{hits = Hits + 1}}.

%%% ---------------------------------------------------------------------------
%%% Synchronous cache handlers.
%%% ---------------------------------------------------------------------------

%% @doc Generate a key/value entry in the cache_server's process.
handle_call({?CACHE_GENERATE, Key},
            _From,
            #?CACHE_STATE{name = CacheName,
                          misses = Misses} = State) ->
  ?DEBUG([{?CACHE_GENERATE, State}]),
  NewRecord = CacheName:generate(Key),
  ets:insert(CacheName, NewRecord),
  {reply, NewRecord, State#?CACHE_STATE{misses = Misses + 1}};

%% @doc Poke the supplied record directly into the cache.
handle_call({?CACHE_POKE, NewRecord},
            _From,
            #?CACHE_STATE{name = CacheName,
                          pokes = Pokes} = State) ->
  ?DEBUG([{?CACHE_POKE, State}]),
  ets:insert(CacheName, NewRecord),
  {reply, ?OK_RETURN, State#?CACHE_STATE{pokes = Pokes + 1}};

%% @doc Wipe the supplied record directly from the cache.
handle_call({?CACHE_WIPE, ExistingRecord},
            _From,
            #?CACHE_STATE{name = CacheName,
                          wipes = Wipes} = State) ->
  ?DEBUG([{?CACHE_WIPE, State}]),
  ets:delete_object(CacheName, ExistingRecord),
  {reply, ?OK_RETURN, State#?CACHE_STATE{wipes = Wipes + 1}};

%% @doc Stop the server.
handle_call(?CACHE_STOP, _From, State) ->
   ?DEBUG([?CACHE_STOP]),
  {stop, ?NORMAL_EXIT, ?OK_RETURN(State), State};

%% @doc Get cache server state which shows hits and misses.
handle_call(?CACHE_STATE, _From, State) ->
  ?DEBUG([?CACHE_STATE]),
  {reply, State, State}.

%%% --------------------------------------------------------------------------
%%% Unimplemented behaviour callbacks.
%%% --------------------------------------------------------------------------
code_change(_, _, _) -> throw(?REASON_NOT_IMPLEMENTED).
handle_info(_, _) -> throw(?REASON_NOT_IMPLEMENTED).
